/*
 * jxUtils Project (2014)
 * 
 * by Denilson Edinaldo Pinto
 * 
 * LGPL
 * Copyright (C) 2014
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package org.jxUtils.xml;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URL;

import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeModel;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.jxUtils.check.CheckUtil;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * @author Denilson Edinaldo Pinto
 * @porject jxUtils
 * @package org.jxUtils.xml
 * @date 08/11/2014 - 10:43:41
 */
public final class XMLUtil {
	
	/**
	 * @param document
	 * @param model
	 * @param node
	 * @return element
	 */
	private static Element createTree(final Document document, final TreeModel model, final Object node) {
	
		//
		final String strNode = node.toString().replaceAll("[\\s]", "_");
		
		final Element element = document.createElement(strNode);
		
		//
		for(int index = 0; index < model.getChildCount(node); index++) {
			
			final Object child = model.getChild(node, index);
			
			element.appendChild(createTree(document, model, child));
		}
		
		return element;
	}
	
	/**
	 * @param root
	 * @param showDetails
	 * @param showType
	 * @return {@link DefaultMutableTreeNode}
	 * @see http://cse.unl.edu/~reich/XML/DOM/XML2JTree.java
	 */
	private static DefaultMutableTreeNode createTreeNode(Node root, boolean showDetails, boolean showType) {
	
		DefaultMutableTreeNode node = null;
		
		String type = getNodeType(root);
		String name = root.getNodeName();
		String value = root.getNodeValue();
		
		//
		if(showDetails) {
			
			// 
			if(root.getNodeType() == Node.CDATA_SECTION_NODE || 
				root.getNodeType() == Node.COMMENT_NODE || 
				root.getNodeType() == Node.DOCUMENT_NODE || 
				root.getNodeType() == Node.DOCUMENT_FRAGMENT_NODE || 
				root.getNodeType() == Node.TEXT_NODE
			) {
				name = value;
				value = null;
			}
			
			String nodeText = (showType ? "[" + type + "] " : "") + name + (CheckUtil.isNotNullAndNotAbsoluteEmpty(value) ? " " + value : "");
			
			node = new DefaultMutableTreeNode(nodeText);
			
		}else {
			
			node = new DefaultMutableTreeNode(root.getNodeType() == Node.TEXT_NODE ? value : name);
		}
		
		NamedNodeMap attributes = root.getAttributes();
		
		//
		if(attributes != null && showDetails) {
			
			//
			for(int attIndex = 0; attIndex < attributes.getLength(); attIndex++) {
				
				Node attNode = attributes.item(attIndex);
				
				String attType = getNodeType(attNode);
				String attName = attNode.getNodeName().trim();
				String attValue = attNode.getNodeValue().trim();
				
				//
				if(CheckUtil.isNotNullAndNotAbsoluteEmpty(attValue)) {
					
					node.add(new DefaultMutableTreeNode((showType ? "[" + attType + "] " : "") + attName + "=\"" + attValue + "\""));
				}
			}
		}
		
		//
		if(root.hasChildNodes()) {
			
			NodeList childNodes = root.getChildNodes();
			
			//
			if(childNodes != null) {
				
				//
				for(int childIndex = 0; childIndex < childNodes.getLength(); childIndex++) {
					
					Node childNode = childNodes.item(childIndex);
					
					//
					if(childNode != null) {
						
						//
						if(childNode.getNodeType() == Node.ELEMENT_NODE) {
							
							node.add(createTreeNode(childNode, showDetails, showType));
						}
						
						String childValue = childNode.getNodeValue();
						
						//
						if(CheckUtil.isNotNullAndNotAbsoluteEmpty(childValue)) {
							
							childValue = childValue.trim();
							
							//
							if( !childValue.equals("\n") && !childValue.equals("\r\n")) {
								
								node.add(createTreeNode(childNode, showDetails, showType));
							}
						}
					}
				}
			}
		}
		
		return node;
	}
	
	/**
	 * @param node
	 * @return Node Type
	 * @see http://cse.unl.edu/~reich/XML/DOM/XML2JTree.java
	 */
	private static String getNodeType(Node node) {
	
		switch(node.getNodeType()) {
		
			case Node.ELEMENT_NODE: {
				
				return "Element";
			}
			case Node.ATTRIBUTE_NODE: {
				
				return "Attribute";
			}
			case Node.TEXT_NODE: {
				
				return "Text";
			}
			case Node.CDATA_SECTION_NODE: {
				
				return "CData Section";
			}
			case Node.ENTITY_REFERENCE_NODE: {
				
				return "Entity Reference";
			}
			case Node.ENTITY_NODE: {
				
				return "Entity";
			}
			case Node.PROCESSING_INSTRUCTION_NODE: {
				
				return "Processing Instruction";
			}
			case Node.COMMENT_NODE: {
				
				return "Comment";
			}
			case Node.DOCUMENT_NODE: {
				
				return "Document";
			}
			case Node.DOCUMENT_TYPE_NODE: {
				
				return "Document Type";
			}
			case Node.DOCUMENT_FRAGMENT_NODE: {
				
				return "Document Fragment";
			}
			case Node.NOTATION_NODE: {
				
				return "Notation";
			}
			default: {
				
				break;
			}
		}
		
		return "Unknown";
	}
	
	/**
	 * @param xmlFile
	 * @param showDetails
	 * @param showType
	 * @return {@link DefaultTreeModel}
	 * @throws ParserConfigurationException
	 * @throws SAXException
	 * @throws IOException
	 * @see http://cse.unl.edu/~reich/XML/DOM/XML2JTree.java
	 */
	public static DefaultTreeModel read(File xmlFile, boolean showDetails, boolean showType) throws ParserConfigurationException, SAXException, IOException {
	
		final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		
		factory.setValidating(false);
		
		final DocumentBuilder builder = factory.newDocumentBuilder();
		
		final Document document = builder.parse(xmlFile);
		
		final Node root = (Node)document.getDocumentElement();
		
		final DefaultMutableTreeNode node = createTreeNode(root, showDetails, showType);
		
		return new DefaultTreeModel(node);
	}
	
	/**
	 * @param xmlFile
	 * @param beanClass
	 * @return unmarshaled XML
	 * @throws JAXBException
	 */
	@SuppressWarnings("unchecked")
	public static final <T> T read(final File xmlFile, final Class<T> beanClass) throws JAXBException {
	
		final JAXBContext context = JAXBContext.newInstance(beanClass);
		
		final Unmarshaller unmarshaller = context.createUnmarshaller();
		
		return (T)unmarshaller.unmarshal(xmlFile);
	}
	
	/**
	 * @param xmlStream
	 * @param showDetails
	 * @param showType
	 * @return {@link DefaultTreeModel}
	 * @throws ParserConfigurationException
	 * @throws SAXException
	 * @throws IOException
	 * @see http://cse.unl.edu/~reich/XML/DOM/XML2JTree.java
	 */
	public static DefaultTreeModel read(InputStream xmlStream, boolean showDetails, boolean showType) throws ParserConfigurationException, SAXException, IOException {
	
		final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		
		factory.setValidating(false);
		
		final DocumentBuilder builder = factory.newDocumentBuilder();
		
		final Document document = builder.parse(xmlStream);
		
		final Node root = (Node)document.getDocumentElement();
		
		final DefaultMutableTreeNode node = createTreeNode(root, showDetails, showType);
		
		return new DefaultTreeModel(node);
	}
	
	/**
	 * @param xmlStream
	 * @param beanClass
	 * @return unmarshaled XML
	 * @throws JAXBException
	 */
	@SuppressWarnings("unchecked")
	public static final <T> T read(final InputStream xmlStream, final Class<T> beanClass) throws JAXBException {
	
		final JAXBContext context = JAXBContext.newInstance(beanClass);
		
		final Unmarshaller unmarshaller = context.createUnmarshaller();
		
		return (T)unmarshaller.unmarshal(xmlStream);
	}
	
	/**
	 * @param xmlReader
	 * @param beanClass
	 * @return unmarshaled XML
	 * @throws JAXBException
	 */
	@SuppressWarnings("unchecked")
	public static final <T> T read(final Reader xmlReader, final Class<T> beanClass) throws JAXBException {
	
		final JAXBContext context = JAXBContext.newInstance(beanClass);
		
		final Unmarshaller unmarshaller = context.createUnmarshaller();
		
		return (T)unmarshaller.unmarshal(xmlReader);
	}
	
	/**
	 * @param xmlURI
	 * @param showDetails
	 * @param showType
	 * @return {@link DefaultTreeModel}
	 * @throws ParserConfigurationException
	 * @throws SAXException
	 * @throws IOException
	 * @see http://cse.unl.edu/~reich/XML/DOM/XML2JTree.java
	 */
	public static DefaultTreeModel read(String xmlURI, boolean showDetails, boolean showType) throws ParserConfigurationException, SAXException, IOException {
	
		final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		
		factory.setValidating(false);
		
		final DocumentBuilder builder = factory.newDocumentBuilder();
		
		final Document document = builder.parse(xmlURI);
		
		final Node root = (Node)document.getDocumentElement();
		
		final DefaultMutableTreeNode node = createTreeNode(root, showDetails, showType);
		
		return new DefaultTreeModel(node);
	}
	
	/**
	 * @param xmlUrl
	 * @param beanClass
	 * @return unmarshaled XML
	 * @throws JAXBException
	 */
	@SuppressWarnings("unchecked")
	public static final <T> T read(final URL xmlUrl, final Class<T> beanClass) throws JAXBException {
	
		final JAXBContext context = JAXBContext.newInstance(beanClass);
		
		final Unmarshaller unmarshaller = context.createUnmarshaller();
		
		return (T)unmarshaller.unmarshal(xmlUrl);
	}
	
	/**
	 * @param treeModel
	 * @return xml String
	 * @throws ParserConfigurationException
	 * @throws TransformerException
	 */
	public static String toXML(TreeModel treeModel) throws ParserConfigurationException, TransformerException {
	
		final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
		final DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
		final DOMImplementation domImplementation = documentBuilder.getDOMImplementation();
		final Document document = domImplementation.createDocument(null, null, null);
		final Element rootElement = createTree(document, treeModel, treeModel.getRoot());
		
		document.appendChild(rootElement);
		
		final DOMSource domSource = new DOMSource(document);
		final TransformerFactory transformerFactory = TransformerFactory.newInstance();
		final Transformer transformer = transformerFactory.newTransformer();
		
		transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
		transformer.setOutputProperty(OutputKeys.METHOD, "xml");
		transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
		transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
		transformer.setOutputProperty(OutputKeys.INDENT, "yes");
		
		final StringWriter stringWriter = new StringWriter();
		
		transformer.transform(domSource, new StreamResult(stringWriter));
		
		return stringWriter.toString();
	}
	
	/**
	 * @param bean
	 * @param xmlFile
	 * @throws JAXBException
	 */
	public static final <T> void write(final T bean, final File xmlFile) throws JAXBException {
	
		write(bean, xmlFile, true);
	}
	
	/**
	 * @param bean
	 * @param xmlFile
	 * @param formated
	 * @throws JAXBException
	 */
	public static final <T> void write(final T bean, final File xmlFile, final boolean formated) throws JAXBException {
	
		final JAXBContext context = JAXBContext.newInstance(bean.getClass());
		
		final Marshaller marshaller = context.createMarshaller();
		
		marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
		
		marshaller.marshal(bean, xmlFile);
	}
	
	/**
	 * @param bean
	 * @param xmlStream
	 * @throws JAXBException
	 */
	public static final <T> void write(final T bean, final OutputStream xmlStream) throws JAXBException {
	
		write(bean, xmlStream, true);
	}
	
	/**
	 * @param bean
	 * @param xmlStream
	 * @param formated
	 * @throws JAXBException
	 */
	public static final <T> void write(final T bean, final OutputStream xmlStream, final boolean formated) throws JAXBException {
	
		final JAXBContext context = JAXBContext.newInstance(bean.getClass());
		
		final Marshaller marshaller = context.createMarshaller();
		
		marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
		
		marshaller.marshal(bean, xmlStream);
	}
	
	/**
	 * @param bean
	 * @param xmlWriter
	 * @throws JAXBException
	 */
	public static final <T> void write(final T bean, final Writer xmlWriter) throws JAXBException {
	
		write(bean, xmlWriter, true);
	}
	
	/**
	 * @param bean
	 * @param xmlWriter
	 * @param formated
	 * @throws JAXBException
	 */
	public static final <T> void write(final T bean, final Writer xmlWriter, final boolean formated) throws JAXBException {
	
		final JAXBContext context = JAXBContext.newInstance(bean.getClass());
		
		final Marshaller marshaller = context.createMarshaller();
		
		marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
		
		marshaller.marshal(bean, xmlWriter);
	}
}
